/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.contactslist.ui;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Contacts.Photo;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.TextAppearanceSpan;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AlphabetIndexer;
import android.widget.ListView;
import android.widget.QuickContactBadge;
import android.widget.SearchView;
import android.widget.SectionIndexer;
import android.widget.TextView;
import com.example.android.contactslist.BuildConfig;
import com.example.android.contactslist.R;
import com.example.android.contactslist.util.ImageLoader;
import com.example.android.contactslist.util.Utils;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Locale;
/**
* This fragment displays a list of contacts stored in the Contacts Provider. Each item in the list
* shows the contact's thumbnail photo and display name. On devices with large screens, this
* fragment's UI appears as part of a two-pane layout, along with the UI of
* {@link ContactDetailFragment}. On smaller screens, this fragment's UI appears as a single pane.
*
* This Fragment retrieves contacts based on a search string. If the user doesn't enter a search
* string, then the list contains all the contacts in the Contacts Provider. If the user enters a
* search string, then the list contains only those contacts whose data matches the string. The
* Contacts Provider itself controls the matching algorithm, which is a "substring" search: if the
* search string is a substring of any of the contacts data, then there is a match.
*
* On newer API platforms, the search is implemented in a SearchView in the ActionBar; as the user
* types the search string, the list automatically refreshes to display results ("type to filter").
* On older platforms, the user must enter the full string and trigger the search. In response, the
* trigger starts a new Activity which loads a fresh instance of this fragment. The resulting UI
* displays the filtered list and disables the search feature to prevent furthering searching.
*/
public class ContactsListFragment extends ListFragment implements
AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor> {
// Defines a tag for identifying log entries
private static final String TAG = "ContactsListFragment";
// Bundle key for saving previously selected search result item
private static final String STATE_PREVIOUSLY_SELECTED_KEY =
"com.example.android.contactslist.ui.SELECTED_ITEM";
private ContactsAdapter mAdapter; // The main query adapter
private ImageLoader mImageLoader; // Handles loading the contact image in a background thread
private String mSearchTerm; // Stores the current search query term
// Contact selected listener that allows the activity holding this fragment to be notified of
// a contact being selected
private OnContactsInteractionListener mOnContactSelectedListener;
// Stores the previously selected search item so that on a configuration change the same item
// can be reselected again
private int mPreviouslySelectedSearchItem = 0;
// Whether or not the search query has changed since the last time the loader was refreshed
private boolean mSearchQueryChanged;
// Whether or not this fragment is showing in a two-pane layout
private boolean mIsTwoPaneLayout;
// Whether or not this is a search result view of this fragment, only used on pre-honeycomb
// OS versions as search results are shown in-line via Action Bar search from honeycomb onward
private boolean mIsSearchResultView = false;
/**
* Fragments require an empty constructor.
*/
public ContactsListFragment() {}
/**
* In platform versions prior to Android 3.0, the ActionBar and SearchView are not supported,
* and the UI gets the search string from an EditText. However, the fragment doesn't allow
* another search when search results are already showing. This would confuse the user, because
* the resulting search would re-query the Contacts Provider instead of searching the listed
* results. This method sets the search query and also a boolean that tracks if this Fragment
* should be displayed as a search result view or not.
*
* @param query The contacts search query.
*/
public void setSearchQuery(String query) {
if (TextUtils.isEmpty(query)) {
mIsSearchResultView = false;
} else {
mSearchTerm = query;
mIsSearchResultView = true;
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Check if this fragment is part of a two-pane set up or a single pane by reading a
// boolean from the application resource directories. This lets allows us to easily specify
// which screen sizes should use a two-pane layout by setting this boolean in the
// corresponding resource size-qualified directory.
mIsTwoPaneLayout = getResources().getBoolean(R.bool.has_two_panes);
// Let this fragment contribute menu items
setHasOptionsMenu(true);
// Create the main contacts adapter
mAdapter = new ContactsAdapter(getActivity());
if (savedInstanceState != null) {
// If we're restoring state after this fragment was recreated then
// retrieve previous search term and previously selected search
// result.
mSearchTerm = savedInstanceState.getString(SearchManager.QUERY);
mPreviouslySelectedSearchItem =
savedInstanceState.getInt(STATE_PREVIOUSLY_SELECTED_KEY, 0);
}
/*
* An ImageLoader object loads and resizes an image in the background and binds it to the
* QuickContactBadge in each item layout of the ListView. ImageLoader implements memory
* caching for each image, which substantially improves refreshes of the ListView as the
* user scrolls through it.
*
* To learn more about downloading images asynchronously and caching the results, read the
* Android training class Displaying Bitmaps Efficiently.
*
* http://developer.android.com/training/displaying-bitmaps/
*/
mImageLoader = new ImageLoader(getActivity(), getListPreferredItemHeight()) {
@Override
protected Bitmap processBitmap(Object data) {
// This gets called in a background thread and passed the data from
// ImageLoader.loadImage().
return loadContactPhotoThumbnail((String) data, getImageSize());
}
};
// Set a placeholder loading image for the image loader
mImageLoader.setLoadingImage(R.drawable.ic_contact_picture_holo_light);
// Add a cache to the image loader
mImageLoader.addImageCache(getActivity().getSupportFragmentManager(), 0.1f);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the list fragment layout
return inflater.inflate(R.layout.contact_list_fragment, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Set up ListView, assign adapter and set some listeners. The adapter was previously
// created in onCreate().
setListAdapter(mAdapter);
getListView().setOnItemClickListener(this);
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView absListView, int scrollState) {
// Pause image loader to ensure smoother scrolling when flinging
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
mImageLoader.setPauseWork(true);
} else {
mImageLoader.setPauseWork(false);
}
}
@Override
public void onScroll(AbsListView absListView, int i, int i1, int i2) {}
});
if (mIsTwoPaneLayout) {
// In a two-pane layout, set choice mode to single as there will be two panes
// when an item in the ListView is selected it should remain highlighted while
// the content shows in the second pane.
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
}
// If there's a previously selected search item from a saved state then don't bother
// initializing the loader as it will be restarted later when the query is populated into
// the action bar search view (see onQueryTextChange() in onCreateOptionsMenu()).
if (mPreviouslySelectedSearchItem == 0) {
// Initialize the loader, and create a loader identified by ContactsQuery.QUERY_ID
getLoaderManager().initLoader(ContactsQuery.QUERY_ID, null, this);
}
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
// Assign callback listener which the holding activity must implement. This is used
// so that when a contact item is interacted with (selected by the user) the holding
// activity will be notified and can take further action such as populating the contact
// detail pane (if in multi-pane layout) or starting a new activity with the contact
// details (single pane layout).
mOnContactSelectedListener = (OnContactsInteractionListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnContactsInteractionListener");
}
}
@Override
public void onPause() {
super.onPause();
// In the case onPause() is called during a fling the image loader is
// un-paused to let any remaining background work complete.
mImageLoader.setPauseWork(false);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
// Gets the Cursor object currently bound to the ListView
final Cursor cursor = mAdapter.getCursor();
// Moves to the Cursor row corresponding to the ListView item that was clicked
cursor.moveToPosition(position);
// Creates a contact lookup Uri from contact ID and lookup_key
final Uri uri = Contacts.getLookupUri(
cursor.getLong(ContactsQuery.ID),
cursor.getString(ContactsQuery.LOOKUP_KEY));
// Notifies the parent activity that the user selected a contact. In a two-pane layout, the
// parent activity loads a ContactDetailFragment that displays the details for the selected
// contact. In a single-pane layout, the parent activity starts a new activity that
// displays contact details in its own Fragment.
mOnContactSelectedListener.onContactSelected(uri);
// If two-pane layout sets the selected item to checked so it remains highlighted. In a
// single-pane layout a new activity is started so this is not needed.
if (mIsTwoPaneLayout) {
getListView().setItemChecked(position, true);
}
}
/**
* Called when ListView selection is cleared, for example
* when search mode is finished and the currently selected
* contact should no longer be selected.
*/
private void onSelectionCleared() {
// Uses callback to notify activity this contains this fragment
mOnContactSelectedListener.onSelectionCleared();
// Clears currently checked item
getListView().clearChoices();
}
// This method uses APIs from newer OS versions than the minimum that this app supports. This
// annotation tells Android lint that they are properly guarded so they won't run on older OS
// versions and can be ignored by lint.
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// Inflate the menu items
inflater.inflate(R.menu.contact_list_menu, menu);
// Locate the search item
MenuItem searchItem = menu.findItem(R.id.menu_search);
// In versions prior to Android 3.0, hides the search item to prevent additional
// searches. In Android 3.0 and later, searching is done via a SearchView in the ActionBar.
// Since the search doesn't create a new Activity to do the searching, the menu item
// doesn't need to be turned off.
if (mIsSearchResultView) {
searchItem.setVisible(false);
}
// In version 3.0 and later, sets up and configures the ActionBar SearchView
if (Utils.hasHoneycomb()) {
// Retrieves the system search manager service
final SearchManager searchManager =
(SearchManager) getActivity().getSystemService(Context.SEARCH_SERVICE);
// Retrieves the SearchView from the search menu item
final SearchView searchView = (SearchView) searchItem.getActionView();
// Assign searchable info to SearchView
searchView.setSearchableInfo(
searchManager.getSearchableInfo(getActivity().getComponentName()));
// Set listeners for SearchView
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String queryText) {
// Nothing needs to happen when the user submits the search string
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
// Called when the action bar search text has changed. Updates
// the search filter, and restarts the loader to do a new query
// using the new search string.
String newFilter = !TextUtils.isEmpty(newText) ? newText : null;
// Don't do anything if the filter is empty
if (mSearchTerm == null && newFilter == null) {
return true;
}
// Don't do anything if the new filter is the same as the current filter
if (mSearchTerm != null && mSearchTerm.equals(newFilter)) {
return true;
}
// Updates current filter to new filter
mSearchTerm = newFilter;
// Restarts the loader. This triggers onCreateLoader(), which builds the
// necessary content Uri from mSearchTerm.
mSearchQueryChanged = true;
getLoaderManager().restartLoader(
ContactsQuery.QUERY_ID, null, ContactsListFragment.this);
return true;
}
});
if (Utils.hasICS()) {
// This listener added in ICS
searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(MenuItem menuItem) {
// Nothing to do when the action item is expanded
return true;
}
@Override
public boolean onMenuItemActionCollapse(MenuItem menuItem) {
// When the user collapses the SearchView the current search string is
// cleared and the loader restarted.
if (!TextUtils.isEmpty(mSearchTerm)) {
onSelectionCleared();
}
mSearchTerm = null;
getLoaderManager().restartLoader(
ContactsQuery.QUERY_ID, null, ContactsListFragment.this);
return true;
}
});
}
if (mSearchTerm != null) {
// If search term is already set here then this fragment is
// being restored from a saved state and the search menu item
// needs to be expanded and populated again.
// Stores the search term (as it will be wiped out by
// onQueryTextChange() when the menu item is expanded).
final String savedSearchTerm = mSearchTerm;
// Expands the search menu item
if (Utils.hasICS()) {
searchItem.expandActionView();
}
// Sets the SearchView to the previous search string
searchView.setQuery(savedSearchTerm, false);
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (!TextUtils.isEmpty(mSearchTerm)) {
// Saves the current search string
outState.putString(SearchManager.QUERY, mSearchTerm);
// Saves the currently selected contact
outState.putInt(STATE_PREVIOUSLY_SELECTED_KEY, getListView().getCheckedItemPosition());
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
// Sends a request to the People app to display the create contact screen
case R.id.menu_add_contact:
final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
startActivity(intent);
break;
// For platforms earlier than Android 3.0, triggers the search activity
case R.id.menu_search:
if (!Utils.hasHoneycomb()) {
getActivity().onSearchRequested();
}
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// If this is the loader for finding contacts in the Contacts Provider
// (the only one supported)
if (id == ContactsQuery.QUERY_ID) {
Uri contentUri;
// There are two types of searches, one which displays all contacts and
// one which filters contacts by a search query. If mSearchTerm is set
// then a search query has been entered and the latter should be used.
if (mSearchTerm == null) {
// Since there's no search string, use the content URI that searches the entire
// Contacts table
contentUri = ContactsQuery.CONTENT_URI;
} else {
// Since there's a search string, use the special content Uri that searches the
// Contacts table. The URI consists of a base Uri and the search string.
contentUri =
Uri.withAppendedPath(ContactsQuery.FILTER_URI, Uri.encode(mSearchTerm));
}
// Returns a new CursorLoader for querying the Contacts table. No arguments are used
// for the selection clause. The search string is either encoded onto the content URI,
// or no contacts search string is used. The other search criteria are constants. See
// the ContactsQuery interface.
return new CursorLoader(getActivity(),
contentUri,
ContactsQuery.PROJECTION,
ContactsQuery.SELECTION,
null,
ContactsQuery.SORT_ORDER);
}
Log.e(TAG, "onCreateLoader - incorrect ID provided (" + id + ")");
return null;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// This swaps the new cursor into the adapter.
if (loader.getId() == ContactsQuery.QUERY_ID) {
mAdapter.swapCursor(data);
// If this is a two-pane layout and there is a search query then
// there is some additional work to do around default selected
// search item.
if (mIsTwoPaneLayout && !TextUtils.isEmpty(mSearchTerm) && mSearchQueryChanged) {
// Selects the first item in results, unless this fragment has
// been restored from a saved state (like orientation change)
// in which case it selects the previously selected search item.
if (data != null && data.moveToPosition(mPreviouslySelectedSearchItem)) {
// Creates the content Uri for the previously selected contact by appending the
// contact's ID to the Contacts table content Uri
final Uri uri = Uri.withAppendedPath(
Contacts.CONTENT_URI, String.valueOf(data.getLong(ContactsQuery.ID)));
mOnContactSelectedListener.onContactSelected(uri);
getListView().setItemChecked(mPreviouslySelectedSearchItem, true);
} else {
// No results, clear selection.
onSelectionCleared();
}
// Only restore from saved state one time. Next time fall back
// to selecting first item. If the fragment state is saved again
// then the currently selected item will once again be saved.
mPreviouslySelectedSearchItem = 0;
mSearchQueryChanged = false;
}
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() == ContactsQuery.QUERY_ID) {
// When the loader is being reset, clear the cursor from the adapter. This allows the
// cursor resources to be freed.
mAdapter.swapCursor(null);
}
}
/**
* Gets the preferred height for each item in the ListView, in pixels, after accounting for
* screen density. ImageLoader uses this value to resize thumbnail images to match the ListView
* item height.
*
* @return The preferred height in pixels, based on the current theme.
*/
private int getListPreferredItemHeight() {
final TypedValue typedValue = new TypedValue();
// Resolve list item preferred height theme attribute into typedValue
getActivity().getTheme().resolveAttribute(
android.R.attr.listPreferredItemHeight, typedValue, true);
// Create a new DisplayMetrics object
final DisplayMetrics metrics = new android.util.DisplayMetrics();
// Populate the DisplayMetrics
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
// Return theme value based on DisplayMetrics
return (int) typedValue.getDimension(metrics);
}
/**
* Decodes and scales a contact's image from a file pointed to by a Uri in the contact's data,
* and returns the result as a Bitmap. The column that contains the Uri varies according to the
* platform version.
*
* @param photoData For platforms prior to Android 3.0, provide the Contact._ID column value.
* For Android 3.0 and later, provide the Contact.PHOTO_THUMBNAIL_URI value.
* @param imageSize The desired target width and height of the output image in pixels.
* @return A Bitmap containing the contact's image, resized to fit the provided image size. If
* no thumbnail exists, returns null.
*/
private Bitmap loadContactPhotoThumbnail(String photoData, int imageSize) {
// Ensures the Fragment is still added to an activity. As this method is called in a
// background thread, there's the possibility the Fragment is no longer attached and
// added to an activity. If so, no need to spend resources loading the contact photo.
if (!isAdded() || getActivity() == null) {
return null;
}
// Instantiates an AssetFileDescriptor. Given a content Uri pointing to an image file, the
// ContentResolver can return an AssetFileDescriptor for the file.
AssetFileDescriptor afd = null;
// This "try" block catches an Exception if the file descriptor returned from the Contacts
// Provider doesn't point to an existing file.
try {
Uri thumbUri;
// If Android 3.0 or later, converts the Uri passed as a string to a Uri object.
if (Utils.hasHoneycomb()) {
thumbUri = Uri.parse(photoData);
} else {
// For versions prior to Android 3.0, appends the string argument to the content
// Uri for the Contacts table.
final Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_URI, photoData);
// Appends the content Uri for the Contacts.Photo table to the previously
// constructed contact Uri to yield a content URI for the thumbnail image
thumbUri = Uri.withAppendedPath(contactUri, Photo.CONTENT_DIRECTORY);
}
// Retrieves a file descriptor from the Contacts Provider. To learn more about this
// feature, read the reference documentation for
// ContentResolver#openAssetFileDescriptor.
afd = getActivity().getContentResolver().openAssetFileDescriptor(thumbUri, "r");
// Gets a FileDescriptor from the AssetFileDescriptor. A BitmapFactory object can
// decode the contents of a file pointed to by a FileDescriptor into a Bitmap.
FileDescriptor fileDescriptor = afd.getFileDescriptor();
if (fileDescriptor != null) {
// Decodes a Bitmap from the image pointed to by the FileDescriptor, and scales it
// to the specified width and height
return ImageLoader.decodeSampledBitmapFromDescriptor(
fileDescriptor, imageSize, imageSize);
}
} catch (FileNotFoundException e) {
// If the file pointed to by the thumbnail URI doesn't exist, or the file can't be
// opened in "read" mode, ContentResolver.openAssetFileDescriptor throws a
// FileNotFoundException.
if (BuildConfig.DEBUG) {
Log.d(TAG, "Contact photo thumbnail not found for contact " + photoData
+ ": " + e.toString());
}
} finally {
// If an AssetFileDescriptor was returned, try to close it
if (afd != null) {
try {
afd.close();
} catch (IOException e) {
// Closing a file descriptor might cause an IOException if the file is
// already closed. Nothing extra is needed to handle this.
}
}
}
// If the decoding failed, returns null
return null;
}
/**
* This is a subclass of CursorAdapter that supports binding Cursor columns to a view layout.
* If those items are part of search results, the search string is marked by highlighting the
* query text. An {@link AlphabetIndexer} is used to allow quicker navigation up and down the
* ListView.
*/
private class ContactsAdapter extends CursorAdapter implements SectionIndexer {
private LayoutInflater mInflater; // Stores the layout inflater
private AlphabetIndexer mAlphabetIndexer; // Stores the AlphabetIndexer instance
private TextAppearanceSpan highlightTextSpan; // Stores the highlight text appearance style
/**
* Instantiates a new Contacts Adapter.
* @param context A context that has access to the app's layout.
*/
public ContactsAdapter(Context context) {
super(context, null, 0);
// Stores inflater for use later
mInflater = LayoutInflater.from(context);
// Loads a string containing the English alphabet. To fully localize the app, provide a
// strings.xml file in res/values-<x> directories, where <x> is a locale. In the file,
// define a string with android:name="alphabet" and contents set to all of the
// alphabetic characters in the language in their proper sort order, in upper case if
// applicable.
final String alphabet = context.getString(R.string.alphabet);
// Instantiates a new AlphabetIndexer bound to the column used to sort contact names.
// The cursor is left null, because it has not yet been retrieved.
mAlphabetIndexer = new AlphabetIndexer(null, ContactsQuery.SORT_KEY, alphabet);
// Defines a span for highlighting the part of a display name that matches the search
// string
highlightTextSpan = new TextAppearanceSpan(getActivity(), R.style.searchTextHiglight);
}
/**
* Identifies the start of the search string in the display name column of a Cursor row.
* E.g. If displayName was "Adam" and search query (mSearchTerm) was "da" this would
* return 1.
*
* @param displayName The contact display name.
* @return The starting position of the search string in the display name, 0-based. The
* method returns -1 if the string is not found in the display name, or if the search
* string is empty or null.
*/
private int indexOfSearchQuery(String displayName) {
if (!TextUtils.isEmpty(mSearchTerm)) {
return displayName.toLowerCase(Locale.getDefault()).indexOf(
mSearchTerm.toLowerCase(Locale.getDefault()));
}
return -1;
}
/**
* Overrides newView() to inflate the list item views.
*/
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
// Inflates the list item layout.
final View itemLayout =
mInflater.inflate(R.layout.contact_list_item, viewGroup, false);
// Creates a new ViewHolder in which to store handles to each view resource. This
// allows bindView() to retrieve stored references instead of calling findViewById for
// each instance of the layout.
final ViewHolder holder = new ViewHolder();
holder.text1 = (TextView) itemLayout.findViewById(android.R.id.text1);
holder.text2 = (TextView) itemLayout.findViewById(android.R.id.text2);
holder.icon = (QuickContactBadge) itemLayout.findViewById(android.R.id.icon);
// Stores the resourceHolder instance in itemLayout. This makes resourceHolder
// available to bindView and other methods that receive a handle to the item view.
itemLayout.setTag(holder);
// Returns the item layout view
return itemLayout;
}
/**
* Binds data from the Cursor to the provided view.
*/
@Override
public void bindView(View view, Context context, Cursor cursor) {
// Gets handles to individual view resources
final ViewHolder holder = (ViewHolder) view.getTag();
// For Android 3.0 and later, gets the thumbnail image Uri from the current Cursor row.
// For platforms earlier than 3.0, this isn't necessary, because the thumbnail is
// generated from the other fields in the row.
final String photoUri = cursor.getString(ContactsQuery.PHOTO_THUMBNAIL_DATA);
final String displayName = cursor.getString(ContactsQuery.DISPLAY_NAME);
final int startIndex = indexOfSearchQuery(displayName);
if (startIndex == -1) {
// If the user didn't do a search, or the search string didn't match a display
// name, show the display name without highlighting
holder.text1.setText(displayName);
if (TextUtils.isEmpty(mSearchTerm)) {
// If the search search is empty, hide the second line of text
holder.text2.setVisibility(View.GONE);
} else {
// Shows a second line of text that indicates the search string matched
// something other than the display name
holder.text2.setVisibility(View.VISIBLE);
}
} else {
// If the search string matched the display name, applies a SpannableString to
// highlight the search string with the displayed display name
// Wraps the display name in the SpannableString
final SpannableString highlightedName = new SpannableString(displayName);
// Sets the span to start at the starting point of the match and end at "length"
// characters beyond the starting point
highlightedName.setSpan(highlightTextSpan, startIndex,
startIndex + mSearchTerm.length(), 0);
// Binds the SpannableString to the display name View object
holder.text1.setText(highlightedName);
// Since the search string matched the name, this hides the secondary message
holder.text2.setVisibility(View.GONE);
}
// Processes the QuickContactBadge. A QuickContactBadge first appears as a contact's
// thumbnail image with styling that indicates it can be touched for additional
// information. When the user clicks the image, the badge expands into a dialog box
// containing the contact's details and icons for the built-in apps that can handle
// each detail type.
// Generates the contact lookup Uri
final Uri contactUri = Contacts.getLookupUri(
cursor.getLong(ContactsQuery.ID),
cursor.getString(ContactsQuery.LOOKUP_KEY));
// Binds the contact's lookup Uri to the QuickContactBadge
holder.icon.assignContactUri(contactUri);
// Loads the thumbnail image pointed to by photoUri into the QuickContactBadge in a
// background worker thread
mImageLoader.loadImage(photoUri, holder.icon);
}
/**
* Overrides swapCursor to move the new Cursor into the AlphabetIndex as well as the
* CursorAdapter.
*/
@Override
public Cursor swapCursor(Cursor newCursor) {
// Update the AlphabetIndexer with new cursor as well
mAlphabetIndexer.setCursor(newCursor);
return super.swapCursor(newCursor);
}
/**
* An override of getCount that simplifies accessing the Cursor. If the Cursor is null,
* getCount returns zero. As a result, no test for Cursor == null is needed.
*/
@Override
public int getCount() {
if (getCursor() == null) {
return 0;
}
return super.getCount();
}
/**
* Defines the SectionIndexer.getSections() interface.
*/
@Override
public Object[] getSections() {
return mAlphabetIndexer.getSections();
}
/**
* Defines the SectionIndexer.getPositionForSection() interface.
*/
@Override
public int getPositionForSection(int i) {
if (getCursor() == null) {
return 0;
}
return mAlphabetIndexer.getPositionForSection(i);
}
/**
* Defines the SectionIndexer.getSectionForPosition() interface.
*/
@Override
public int getSectionForPosition(int i) {
if (getCursor() == null) {
return 0;
}
return mAlphabetIndexer.getSectionForPosition(i);
}
/**
* A class that defines fields for each resource ID in the list item layout. This allows
* ContactsAdapter.newView() to store the IDs once, when it inflates the layout, instead of
* calling findViewById in each iteration of bindView.
*/
private class ViewHolder {
TextView text1;
TextView text2;
QuickContactBadge icon;
}
}
/**
* This interface must be implemented by any activity that loads this fragment. When an
* interaction occurs, such as touching an item from the ListView, these callbacks will
* be invoked to communicate the event back to the activity.
*/
public interface OnContactsInteractionListener {
/**
* Called when a contact is selected from the ListView.
* @param contactUri The contact Uri.
*/
public void onContactSelected(Uri contactUri);
/**
* Called when the ListView selection is cleared like when
* a contact search is taking place or is finishing.
*/
public void onSelectionCleared();
}
/**
* This interface defines constants for the Cursor and CursorLoader, based on constants defined
* in the {@link android.provider.ContactsContract.Contacts} class.
*/
public interface ContactsQuery {
// An identifier for the loader
final static int QUERY_ID = 1;
// A content URI for the Contacts table
final static Uri CONTENT_URI = Contacts.CONTENT_URI;
// The search/filter query Uri
final static Uri FILTER_URI = Contacts.CONTENT_FILTER_URI;
// The selection clause for the CursorLoader query. The search criteria defined here
// restrict results to contacts that have a display name and are linked to visible groups.
// Notice that the search on the string provided by the user is implemented by appending
// the search string to CONTENT_FILTER_URI.
@SuppressLint("InlinedApi")
final static String SELECTION =
(Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME) +
"<>''" + " AND " + Contacts.IN_VISIBLE_GROUP + "=1";
// The desired sort order for the returned Cursor. In Android 3.0 and later, the primary
// sort key allows for localization. In earlier versions. use the display name as the sort
// key.
@SuppressLint("InlinedApi")
final static String SORT_ORDER =
Utils.hasHoneycomb() ? Contacts.SORT_KEY_PRIMARY : Contacts.DISPLAY_NAME;
// The projection for the CursorLoader query. This is a list of columns that the Contacts
// Provider should return in the Cursor.
@SuppressLint("InlinedApi")
final static String[] PROJECTION = {
// The contact's row id
Contacts._ID,
// A pointer to the contact that is guaranteed to be more permanent than _ID. Given
// a contact's current _ID value and LOOKUP_KEY, the Contacts Provider can generate
// a "permanent" contact URI.
Contacts.LOOKUP_KEY,
// In platform version 3.0 and later, the Contacts table contains
// DISPLAY_NAME_PRIMARY, which either contains the contact's displayable name or
// some other useful identifier such as an email address. This column isn't
// available in earlier versions of Android, so you must use Contacts.DISPLAY_NAME
// instead.
Utils.hasHoneycomb() ? Contacts.DISPLAY_NAME_PRIMARY : Contacts.DISPLAY_NAME,
// In Android 3.0 and later, the thumbnail image is pointed to by
// PHOTO_THUMBNAIL_URI. In earlier versions, there is no direct pointer; instead,
// you generate the pointer from the contact's ID value and constants defined in
// android.provider.ContactsContract.Contacts.
Utils.hasHoneycomb() ? Contacts.PHOTO_THUMBNAIL_URI : Contacts._ID,
// The sort order column for the returned Cursor, used by the AlphabetIndexer
SORT_ORDER,
};
// The query column numbers which map to each value in the projection
final static int ID = 0;
final static int LOOKUP_KEY = 1;
final static int DISPLAY_NAME = 2;
final static int PHOTO_THUMBNAIL_DATA = 3;
final static int SORT_KEY = 4;
}
}